-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
pkg/command: wrap jsonmessage.DisplayJSONMessagesStream
with go context
#5663
base: master
Are you sure you want to change the base?
Conversation
c21438f
to
4884112
Compare
4884112
to
51e16ee
Compare
DisplayJSONMessagesStream
DisplayJSONMessagesToStream
and DisplayJSONMessagesStream
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #5663 +/- ##
==========================================
- Coverage 59.51% 59.44% -0.08%
==========================================
Files 346 347 +1
Lines 29376 29394 +18
==========================================
- Hits 17483 17472 -11
- Misses 10923 10950 +27
- Partials 970 972 +2 |
d55ee6d
to
bfd3131
Compare
bfd3131
to
0298524
Compare
cli/internal/jsonmessage/display.go
Outdated
// DisplayStream prints the JSON messages from the given reader to the given stream. | ||
// | ||
// It wraps the [jsonmessage.DisplayJSONMessagesToStream] function to make it | ||
// "context aware" and appropriately returns why the function was canceled. | ||
// | ||
// It returns an error if the context is canceled, but not if the input reader / stream is closed. | ||
func DisplayStream(ctx context.Context, in io.Reader, stream Stream, auxCallback func(JSONMessage)) error { | ||
select { | ||
case <-ctx.Done(): | ||
return ctx.Err() | ||
default: | ||
err := jsonmessage.DisplayJSONMessagesToStream(in, stream, auxCallback) | ||
if err != nil { | ||
return err | ||
} | ||
if ctx.Done() != nil { | ||
return ctx.Err() | ||
} | ||
} | ||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could drop the select to simplify this a bit
// DisplayStream prints the JSON messages from the given reader to the given stream. | |
// | |
// It wraps the [jsonmessage.DisplayJSONMessagesToStream] function to make it | |
// "context aware" and appropriately returns why the function was canceled. | |
// | |
// It returns an error if the context is canceled, but not if the input reader / stream is closed. | |
func DisplayStream(ctx context.Context, in io.Reader, stream Stream, auxCallback func(JSONMessage)) error { | |
select { | |
case <-ctx.Done(): | |
return ctx.Err() | |
default: | |
err := jsonmessage.DisplayJSONMessagesToStream(in, stream, auxCallback) | |
if err != nil { | |
return err | |
} | |
if ctx.Done() != nil { | |
return ctx.Err() | |
} | |
} | |
return nil | |
} | |
// DisplayStream prints the JSON messages from the given reader to the given stream. | |
// | |
// It wraps the [jsonmessage.DisplayJSONMessagesToStream] function to make it | |
// "context aware" and appropriately returns why the function was canceled. | |
// | |
// It returns an error if the context is canceled, but not if the input reader / stream is closed. | |
func DisplayStream(ctx context.Context, in io.Reader, stream Stream, auxCallback func(JSONMessage)) error { | |
if ctx.Err() != nil { | |
return ctx.Err() | |
} | |
if err := jsonmessage.DisplayJSONMessagesToStream(in, stream, auxCallback); err != nil { | |
return err | |
} | |
return ctx.Err() | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also mostly support the context cancellation by wrapping the in io.Reader
we pass into DisplayJSONMessagesToStream
with something like:
type stopableReader struct {
r io.Reader
closed error
}
func (c *stopableReader) Read(p []byte) (int, error) {
if (c.closed != nil) {
return 0, c.closed
}
return c.r.Read(p)
}
and then set closed = ctx.Err()
after the context is done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think i'd prefer we be explicit about it instead of in the io.Reader
. I'll remove the select
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The io.Reader
wrapper is a separate thing though.
With the current implementation the context cancellation isn't actually handled once the DisplayJSONMessagesToStream
is called, the cancellation won't have any actual effect.
I pushed a small test to your branch to illustrate that.
The reader wrapper is trying to achieve that, that means this would effectively remove the need to make changes to the pkg/jsonmessage
on moby side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(sorry, late to comment; I saw the discussion, and was indeed wondering if closing the reader would make the jsonmessages properly cancel, as ISTR it was originally designed around some of that)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the current implementation the context cancellation isn't actually handled once the DisplayJSONMessagesToStream is called, the cancellation won't have any actual effect.
yes, but the function was never designed for that. This PR is only trying to solve returning a context error instead of just a nil (when the reader is implicitly cancelled by context through say http.NewRequestWithContext
).
The reader wrapper is trying to achieve that, that means this would effectively remove the need to make changes to the pkg/jsonmessage on moby side.
This PR's intent is to circumvent the moby side PR - or at least find an alternative without needing to refactor the function.
and was indeed wondering if closing the reader would make the jsonmessages properly cancel
Yes you are correct, it won't. The jsonmessages
functions rely on the fact that io.EOF
was returned to break the for loop.
EDIT: I meant to say that jsonmessages
functions solely rely on io.EOF
from the reader to exit, but that doesn't mean we will know why it was cancelled (the io.EOF
reason). So closing the reader
would exit the loop. https://github.com/moby/moby/blob/master/pkg/jsonmessage/jsonmessage.go#L237-L242
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will also exit on any Read
other error and will return it:
If the Read
inside Decode
returns context.Canceled then it would also be returned from Decode
.
That's what the stopableReader
above does.
This PR's intent is to circumvent the moby side PR - or at least find an alternative without needing to refactor the function.
I get that, but why not make the DisplayStream
function actually respect the cancellation if we can?
We control the reader passed so we can make it start returning error for subsequent Read
s after the context is done.
With that stopableReader
I posted above, we could just wait for ctx.Done()
channel to be closed and set r.closed = ctx.Err()
to make the reader start returning the context error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i see, alright I'll look into this then
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I mentioned in #5645 (comment) and #5645 (comment), I don't think adding a context to jsonmessage
functions is the best approach – it would be simpler to let those continue only caring about whether the read fails or not to know when to stop instead of having to care about both things.
@vvoland's suggestion allows addressing the issue without needless changes to jsonmessage
.
…ontext Allows for the `jsonmessage.DisplayJSONMessagesToStream` function to correctly return when the context is cancelled with the appropriate reason (`ctx.Error()`) instead of just a nil error. Follow-up to docker@30a73ff Signed-off-by: Alano Terblanche <[email protected]>
0298524
to
9d31dfb
Compare
DisplayJSONMessagesToStream
and DisplayJSONMessagesStream
jsonmessage.DisplayJSONMessagesStream
with go context
Signed-off-by: Paweł Gronowski <[email protected]>
cli/internal/jsonmessage/display.go
Outdated
// "context aware" and appropriately returns why the function was canceled. | ||
// | ||
// It returns an error if the context is canceled, but not if the input reader / stream is closed. | ||
func DisplayStream(ctx context.Context, in io.Reader, stream Stream, auxCallback func(JSONMessage)) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One other thing consider is to add 2 separate functions; one that takes an auxCallBack
and one without; looking at the CLI code, I see that we need the auxCallBack
in only 2 places;
cli/command/container/create.go: return jsonmessage.DisplayJSONMessagesToStream(responseBody, out, nil)
cli/command/image/import.go: return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil)
cli/command/image/load.go: return jsonmessage.DisplayJSONMessagesToStream(response.Body, dockerCli.Out(), nil)
cli/command/image/push.go: err = jsonmessage.DisplayJSONMessagesToStream(responseBody, streams.NewOut(io.Discard), handleAux(dockerCli))
cli/command/image/push.go: return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), handleAux(dockerCli))
cli/command/image/trust.go: if err := jsonmessage.DisplayJSONMessagesToStream(in, ioStreams.Out(), nil); err != nil {
cli/command/image/trust.go: if err := jsonmessage.DisplayJSONMessagesToStream(in, ioStreams.Out(), handleTarget); err != nil {
cli/command/image/trust.go: return jsonmessage.DisplayJSONMessagesToStream(responseBody, out, nil)
cli/command/plugin/install.go: if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil {
cli/command/plugin/push.go: return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil)
cli/command/plugin/upgrade.go: if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil {
cli/command/service/helpers.go: err := jsonmessage.DisplayJSONMessagesToStream(pipeReader, dockerCli.Out(), nil)
cli/command/swarm/ca.go: err := jsonmessage.DisplayJSONMessagesToStream(pipeReader, dockerCli.Out(), nil)
Alternatively, we could consider (for a expandable UX) having it accept a DisplayOptions{}
struct (or fancy functional DisplayOptions()
) , e.g.
err := DisplayWithOptions(ctx, in, stream, Options{
AuxCallback: myCallback,
})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another is to make Display
a variadic function like so:
type Options func(*options)
type options struct {
AuxCallback func(JSONMessage)
}
func WithAuxCallback(cb func(JSONMessage)) Options {
return func(o *options) {
o.AuxCallback = cb
}
}
func Display(ctx context.Context, in io.Reader, stream Stream, opts ...Options) error {
}
Then when you use it, you don't even need to specify the last parameter in most cases.
err := jsonstream.Display(ctx, reader, out)
// or
err := jsonstream.Display(ctx, reader, out, jsonstream.WithAuxCallback(func(j jsonstream.JSONMessage){}))
cli/internal/jsonmessage/display.go
Outdated
@@ -0,0 +1,32 @@ | |||
package jsonmessage |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we could name this package jsonstream
it could help disambiguate with the jsonmessage
package, and we could name the function Display()
as it would be called as jsonstream.Display()
cli/internal/jsonmessage/display.go
Outdated
type ( | ||
Stream = jsonmessage.Stream | ||
JSONMessage = jsonmessage.JSONMessage | ||
JSONError = jsonmessage.JSONError |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still an internal
package, so not too problematic, but it looks like JSONError
is literally used in a single place in the CLI. We may want to have a look if we want to keep it as an exported type, or if there's alternatives to reduce the "public" (but right now, still "internal") API;
cli/cli/command/image/build.go
Lines 366 to 375 in f4a68da
if jerr, ok := err.(*jsonmessage.JSONError); ok { | |
// If no error code is set, default to 1 | |
if jerr.Code == 0 { | |
jerr.Code = 1 | |
} | |
if options.quiet { | |
fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff) | |
} | |
return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea behind this was to keep all the jsonmessage
code in a singular place inside an internal package inside the CLI. Maybe i'm being a bit excessive, but it felt cleaner
This patch returns the `context.Error()` from the `io.Reader`, allowing us to also immediately cancel the wrapped function without waiting for an `io.EOF`. Signed-off-by: Alano Terblanche <[email protected]>
Follow-up to #5639
Allows for the
jsonmessage.DisplayJSONMessagesStream
tocorrectly return when the context is cancelled with the
appropriate error message, instead of just a nil error.
Follow-up to 30a73ff
- What I did
- How I did it
- How to verify it
- Description for the changelog
- A picture of a cute animal (not mandatory but encouraged)